Skip to content

Conversation

@leesewon00
Copy link
Member

@leesewon00 leesewon00 commented Sep 28, 2024

관련 이슈

작업 내용

  • 안정적인 처리를 위해 TreadPoolTaskExecutor 활용 (큐+쓰레드풀)
  • 3MB 이상의 파일 업로드시 리사이징하는 lambda 함수 개발

기존 테스트코드 문제없이 동작합니다.
람다함수 문제없이 동작합니다. (이름은 바꿔두겠습니다.)

특이 사항

내용이 많아 파일로 업로드합니다.
파일업로드_리팩토링_고찰.docx

람다함수는 아래와 같습니다.

lambda_function.py
import boto3
import os
import sys
import uuid
from urllib.parse import unquote_plus
from PIL import Image, ExifTags

s3_client = boto3.client('s3')

def resize_image(image_path, resized_path):
    max_file_size = 3 * 1024 * 1024  # 3MB
    reduction_step = 0.9  # 10%씩 줄임

    with Image.open(image_path) as image:
        # EXIF 데이터에서 회전 정보 읽기
        try:
            for orientation in ExifTags.TAGS.keys():
                if ExifTags.TAGS[orientation] == 'Orientation':
                    break
            exif = image._getexif()
            if exif is not None:
                orientation_value = exif.get(orientation)
                if orientation_value is not None:
                    if orientation_value == 3:
                        image = image.rotate(180, expand=True)
                    elif orientation_value == 6:
                        image = image.rotate(270, expand=True)
                    elif orientation_value == 8:
                        image = image.rotate(90, expand=True)
        except Exception as e:
            print(f"Failed to read EXIF data: {e}")

        # 원본 이미지 포맷 확인
        image_format = image.format
        if image_format is None:
            image_format = 'JPEG'  # 기본적으로 JPEG로 저장

        # 이미지 리사이즈: 10%씩 줄이면서 파일 크기를 확인
        while True:
            # 현재 상태의 이미지 임시로 저장
            image.save(resized_path, image_format)

            # 파일 크기 확인
            file_size = os.path.getsize(resized_path)

            # 파일 크기가 3MB 이하인지 확인
            if file_size <= max_file_size:
                print(f"Final image size: {file_size / 1024 / 1024:.2f} MB")
                break

            # 이미지 크기 90%로 줄이기
            new_size = tuple(int(x * reduction_step) for x in image.size)
            image = image.resize(new_size, Image.ANTIALIAS)

        # 최종 이미지 저장
        image.save(resized_path, image_format)

def lambda_handler(event, context):
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = unquote_plus(record['s3']['object']['key'])
        tmpkey = key.replace('/', '')
        download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey)
        upload_path = '/tmp/resized-{}'.format(tmpkey)

        # S3에서 파일 다운로드
        s3_client.download_file(bucket, key, download_path)

        try:
            # 이미지 리사이즈
            resize_image(download_path, upload_path)

            # 원본 파일의 메타데이터 가져오기
            original_object = s3_client.head_object(Bucket=bucket, Key=key)
            original_content_type = original_object['ContentType']

            # 기존 경로 : origin/profile/,,,
            # 수정 경로 : profile/,,,
            upload_key = key.replace('origin/', '')
            print(f"Uploading resized image to: profile/{upload_key}")

            # 파일을 퍼블릭으로 업로드하고 Content-Type 및 Content-Disposition 설정
            s3_client.upload_file(
                upload_path,
                bucket,
                upload_key,
                ExtraArgs={
                    'ACL': 'public-read',
                    'ContentType': original_content_type,  # 기존 파일의 Content-Type 사용
                    'ContentDisposition': 'inline'  # 브라우저에서 바로 열리도록 설정
                }
            )

            # 기존 파일 삭제
            print(f"Deleting original file: {key}")
            s3_client.delete_object(Bucket=bucket, Key=key)

        except Exception as e:
            print(f"Error processing file {key}: {e}")
            raise e

리뷰 요구사항 (선택)

@wibaek
Copy link
Member

wibaek commented Oct 3, 2024

람다 파이썬 코드에 관련해서

적정 파일 크기를 목표로 잡고 10%씩 리사이즈 하는 방식이 좋은 것 같습니다.

다만 svg나 png 포맷의 방식의 특성을 악용하면 크기 조정으로는 용량이 거의 줄어들지 않아, 해당 방법을 이용하면 람다 실행시간을 매우 길게 끄는 공격을 할 수 있을 것도 같다고 생각을 했는데, 확인해보니 이미 람다 시간이 최대 30초로 조정이 되어 있어서 문제는 없을 것 같네요.

기존 파일 처리도 깔끔하게 되었고, 그리고 메타데이터등은 생각하지 못했는데, 이를 고려하여 코드를 짜신 것도 좋은 것 같습니다.👍

혹시 코드중에 파일업로드시에 ACL 권한을 같이 부여하는 것으로 보이는데, 권한 부여의 이유가 있을까요?

ExtraArgs={
    'ACL': 'public-read',
    'ContentType': original_content_type,  # 기존 파일의 Content-Type 사용
    'ContentDisposition': 'inline'  # 브라우저에서 바로 열리도록 설정
}

각 객체에 권한을 설정해야하는 경우가 아니라면, 에세스 권한은 버킷 정책등을 이용해 일괄적으로 관리하는 것이 좋을 것 같습니다.

@leesewon00
Copy link
Member Author

혹시 코드중에 파일업로드시에 ACL 권한을 같이 부여하는 것으로 보이는데, 권한 부여의 이유가 있을까요?

ExtraArgs={
    'ACL': 'public-read',
    'ContentType': original_content_type,  # 기존 파일의 Content-Type 사용
    'ContentDisposition': 'inline'  # 브라우저에서 바로 열리도록 설정
}

각 객체에 권한을 설정해야하는 경우가 아니라면, 에세스 권한은 버킷 정책등을 이용해 일괄적으로 관리하는 것이 좋을 것 같습니다.

기존 파일 업로드 코드에서 acl public-read 권한을 설정하여 업로드 하기에 cloudfront로 완전히 전환되기 이전까지는 파일을 원활하게 불러오기 위해서는 해당 권한설정이 필요하다고 생각하여 위와같이 작성하였습니다.
전환 이후 버킷상에서 퍼블릭 엑세스를 차단하고, 코드상에서 권한을 부여하는 부분도 삭제하면 좋을 것 같은데 어떻게 생각하시나요?

@wibaek
Copy link
Member

wibaek commented Oct 4, 2024

혹시 코드중에 파일업로드시에 ACL 권한을 같이 부여하는 것으로 보이는데, 권한 부여의 이유가 있을까요?

ExtraArgs={
    'ACL': 'public-read',
    'ContentType': original_content_type,  # 기존 파일의 Content-Type 사용
    'ContentDisposition': 'inline'  # 브라우저에서 바로 열리도록 설정
}

각 객체에 권한을 설정해야하는 경우가 아니라면, 에세스 권한은 버킷 정책등을 이용해 일괄적으로 관리하는 것이 좋을 것 같습니다.

기존 파일 업로드 코드에서 acl public-read 권한을 설정하여 업로드 하기에 cloudfront로 완전히 전환되기 이전까지는 파일을 원활하게 불러오기 위해서는 해당 권한설정이 필요하다고 생각하여 위와같이 작성하였습니다. 전환 이후 버킷상에서 퍼블릭 엑세스를 차단하고, 코드상에서 권한을 부여하는 부분도 삭제하면 좋을 것 같은데 어떻게 생각하시나요?

앗 그렇네요. 원래 ACL을 이용해서 퍼블릭 엑세스를 하고 있었네요. 새롭게 권한관리를 도입한다면 ACL보다 버킷 정책이 권장되는 방식이라 말씀드린건데, 기존에 ACL을 사용하고 있던거라면 말씀하신대로 cloudfront 전환 전까지는 기존 방법을 계속 이용하는게 좋을 것 같습니다!

@leesewon00 leesewon00 merged commit c1ba148 into solid-connection:main Oct 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

스프링단에서 업로드 파일 크기 제한, 게시판 업로드 사진 자동 리사이징 기능

2 participants